import { EventEmitter } from "events";
import { FastifyBaseLogger } from "fastify";
import {
  BrowserFingerprintWithHeaders,
  FingerprintGenerator,
  FingerprintGeneratorOptions,
  VideoCard,
} from "fingerprint-generator";
import { FingerprintInjector } from "fingerprint-injector";
import fs from "fs";
import { IncomingMessage } from "http";
import httpProxy from "http-proxy";
import os from "os";
import path from "path";
import puppeteer, {
  Browser,
  BrowserContext,
  CDPSession,
  HTTPRequest,
  Page,
  Protocol,
  Target,
  TargetType,
} from "puppeteer-core";
import { Duplex } from "stream";
import { env } from "../../env.js";
import { loadFingerprintScript } from "../../scripts/index.js";
import { traceable, tracer } from "../../telemetry/tracer.js";
import { BrowserEventType, BrowserLauncherOptions, EmitEvent } from "../../types/index.js";
import {
  isAdRequest,
  isHeavyMediaRequest,
  isHostBlocked,
  isUrlMatchingPatterns,
  isImageRequest,
} from "../../utils/requests.js";
import { filterHeaders, getChromeExecutablePath, installMouseHelper } from "../../utils/browser.js";
import {
  deepMerge,
  extractStorageForPage,
  getProfilePath,
  groupSessionStorageByOrigin,
  handleFrameNavigated,
} from "../../utils/context.js";
import { getExtensionPaths } from "../../utils/extensions.js";
import { RetryManager, RetryOptions } from "../../utils/retry.js";
import { ChromeContextService } from "../context/chrome-context.service.js";
import { SessionData } from "../context/types.js";
import { FileService } from "../file.service.js";
import {
  BaseLaunchError,
  BrowserProcessError,
  BrowserProcessState,
  CleanupError,
  CleanupType,
  FingerprintError,
  FingerprintStage,
  LaunchTimeoutError,
  NetworkError,
  NetworkOperation,
  PluginError,
  PluginName,
  PluginOperation,
  ResourceError,
  ResourceType,
  SessionContextError,
  SessionContextType,
  categorizeError,
} from "./errors/launch-errors.js";
import { BasePlugin } from "./plugins/core/base-plugin.js";
import { PluginManager } from "./plugins/core/plugin-manager.js";
import { isSimilarConfig, validateLaunchConfig, validateTimezone } from "./utils/validation.js";
import { TargetInstrumentationManager } from "./instrumentation/target-manager.js";
import {
  createBrowserLogger as createInstrumentationLogger,
  BrowserLogger,
} from "./instrumentation/browser-logger.js";
import { executeBestEffort, executeCritical, executeOptional } from "./utils/error-handlers.js";
import { TimezoneFetcher } from "../timezone-fetcher.service.js";

export class CDPService extends EventEmitter {
  private logger: FastifyBaseLogger;
  private keepAlive: boolean;

  private browserInstance: Browser | null;
  private wsEndpoint: string | null;
  private fingerprintData: BrowserFingerprintWithHeaders | null;
  private sessionContext: SessionData | null;
  private chromeExecPath: string;
  private wsProxyServer: httpProxy;
  private primaryPage: Page | null;
  private launchConfig?: BrowserLauncherOptions;
  private defaultLaunchConfig: BrowserLauncherOptions;
  private currentSessionConfig: BrowserLauncherOptions | null;
  private shuttingDown: boolean;
  private defaultTimezone: string;
  private pluginManager: PluginManager;
  private trackedOrigins: Set<string> = new Set<string>();
  private chromeSessionService: ChromeContextService;
  private retryManager: RetryManager;
  private targetInstrumentationManager: TargetInstrumentationManager;
  private instrumentationLogger: BrowserLogger;

  private launchMutators: ((config: BrowserLauncherOptions) => Promise<void> | void)[] = [];
  private shutdownMutators: ((config: BrowserLauncherOptions | null) => Promise<void> | void)[] =
    [];
  private proxyWebSocketHandler:
    | ((req: IncomingMessage, socket: Duplex, head: Buffer) => Promise<void>)
    | null = null;
  private disconnectHandler: () => Promise<void> = () => this.endSession();

  constructor(
    config: { keepAlive?: boolean },
    logger: FastifyBaseLogger,
    storage?: any,
    enableConsoleLogging?: boolean,
  ) {
    super();
    this.logger = logger.child({ component: "CDPService" });
    const { keepAlive = true } = config;

    this.keepAlive = keepAlive;
    this.browserInstance = null;
    this.wsEndpoint = null;
    this.fingerprintData = null;
    this.sessionContext = null;
    this.chromeExecPath = getChromeExecutablePath();
    this.defaultTimezone = env.DEFAULT_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;
    this.trackedOrigins = new Set<string>();
    this.chromeSessionService = new ChromeContextService(logger);
    this.retryManager = new RetryManager(logger);

    this.wsProxyServer = httpProxy.createProxyServer();

    this.wsProxyServer.on("error", (err) => {
      this.logger.error(`Proxy server error: ${err}`);
    });

    this.primaryPage = null;
    this.currentSessionConfig = null;
    this.shuttingDown = false;

    // Initialize timezone fetcher for cold start
    const timezoneFetcher = new TimezoneFetcher(logger);
    const coldStartTimezone = timezoneFetcher.getTimezone(undefined, this.defaultTimezone);

    this.defaultLaunchConfig = {
      options: {
        headless: env.CHROME_HEADLESS,
        args: [],
        ignoreDefaultArgs: ["--enable-automation"],
      },
      blockAds: true,
      extensions: [],
      userDataDir: env.CHROME_USER_DATA_DIR || path.join(os.tmpdir(), "steel-chrome"),
      timezone: coldStartTimezone,
      userPreferences: {
        plugins: {
          always_open_pdf_externally: true,
          plugins_disabled: ["Chrome PDF Viewer"],
        },
      },
      deviceConfig: { device: "desktop" },
    };

    this.pluginManager = new PluginManager(this, logger);

    this.instrumentationLogger = createInstrumentationLogger({
      baseLogger: this.logger,
      initialContext: {},
      storage: storage || null,
      enableConsoleLogging: enableConsoleLogging ?? true,
    });
    this.targetInstrumentationManager = new TargetInstrumentationManager(
      this.instrumentationLogger,
      this.logger,
    );
    this.instrumentationLogger?.on?.(EmitEvent.Log, (event, context) => {
      this.emit(EmitEvent.Log, event);
    });
    this.logger.info("[CDPService] Target instrumentation enabled");
  }

  public getInstrumentationLogger(): BrowserLogger {
    return this.instrumentationLogger;
  }

  public getLogger(name: string) {
    return this.logger.child({ component: name });
  }

  public setProxyWebSocketHandler(
    handler: ((req: IncomingMessage, socket: Duplex, head: Buffer) => Promise<void>) | null,
  ): void {
    this.proxyWebSocketHandler = handler;
  }

  public setDisconnectHandler(handler: () => Promise<void>): void {
    this.disconnectHandler = handler;
  }

  public getBrowserInstance(): Browser | null {
    return this.browserInstance;
  }

  public getLaunchConfig(): BrowserLauncherOptions | undefined {
    return this.launchConfig;
  }

  public getSessionContext(): SessionData | null {
    return this.sessionContext;
  }

  public registerLaunchHook(fn: (config: BrowserLauncherOptions) => Promise<void> | void) {
    this.launchMutators.push(fn);
  }

  public registerShutdownHook(fn: (config: BrowserLauncherOptions | null) => Promise<void> | void) {
    this.shutdownMutators.push(fn);
  }

  private removeAllHandlers() {
    this.browserInstance?.removeAllListeners();
    this.removeAllListeners();
  }

  public isRunning(): boolean {
    return this.browserInstance?.process() !== null;
  }

  public getTargetId(page: Page) {
    //@ts-ignore
    return page.target()._targetId;
  }

  public async getPrimaryPage(): Promise<Page> {
    if (!this.primaryPage || !this.browserInstance) {
      throw new Error("CDPService has not been launched yet!");
    }
    if (this.primaryPage.isClosed()) {
      this.primaryPage = await this.browserInstance.newPage();
    }
    return this.primaryPage;
  }

  private getDebuggerBase(): { baseUrl: string; protocol: string; wsProtocol: string } {
    const baseUrl = env.CDP_DOMAIN ?? env.DOMAIN ?? `${env.HOST}:${env.CDP_REDIRECT_PORT}`;
    const protocol = env.USE_SSL ? "https" : "http";
    const wsProtocol = env.USE_SSL ? "wss" : "ws";
    return { baseUrl, protocol, wsProtocol };
  }

  public getDebuggerUrl() {
    const { baseUrl, protocol } = this.getDebuggerBase();
    return `${protocol}://${baseUrl}/devtools/devtools_app.html`;
  }

  public getDebuggerWsUrl(pageId?: string) {
    const { baseUrl, wsProtocol } = this.getDebuggerBase();
    return `${wsProtocol}://${baseUrl}/devtools/page/${
      pageId ?? this.getTargetId(this.primaryPage!)
    }`;
  }

  public async refreshPrimaryPage() {
    const newPage = await this.createPage();
    if (this.primaryPage) {
      // Notify plugins before page close
      await this.pluginManager.onBeforePageClose(this.primaryPage);
      await this.primaryPage.close();
    }
    this.primaryPage = newPage;
  }

  public registerPlugin(plugin: BasePlugin) {
    return this.pluginManager.register(plugin);
  }

  public unregisterPlugin(pluginName: string) {
    return this.pluginManager.unregister(pluginName);
  }

  private async handleTargetChange(target: Target) {
    if (target.type() !== "page") return;

    const page = await target.page().catch((e) => {
      this.logger.error(`Error handling target change in CDPService: ${e}`);
      return null;
    });

    if (page) {
      this.pluginManager.onPageNavigate(page);

      //@ts-ignore
      const pageId = page.target()._targetId;

      // Track the origin of the page
      try {
        const url = page.url();
        if (url && url.startsWith("http")) {
          const origin = new URL(url).origin;
          this.trackedOrigins.add(origin);
          this.logger.debug(`[CDPService] Tracking new origin: ${origin}`);
        }
      } catch (err) {
        this.logger.error(`[CDPService] Error tracking origin: ${err}`);
      }

      this.emit(EmitEvent.PageId, { pageId });
    }
  }

  private async handleNewTarget(target: Target) {
    try {
      await this.targetInstrumentationManager.attach(target, target.type() as TargetType);
    } catch (error) {
      this.logger.error({ err: error }, `[CDPService] Error attaching target instrumentation`);
    }

    if (target.type() === TargetType.PAGE) {
      const page = await target.page().catch((e) => {
        this.logger.error(`Error handling new target in CDPService: ${e}`);
        return null;
      });

      if (page) {
        try {
          const url = page.url();
          if (url && url.startsWith("http")) {
            const origin = new URL(url).origin;
            this.trackedOrigins.add(origin);
            this.logger.debug(`[CDPService] Tracking new origin: ${origin}`);
          }
        } catch (err) {
          this.logger.error(`[CDPService] Error tracking origin: ${err}`);
        }

        // Notify plugins about the new page
        await this.pluginManager.onPageCreated(page);

        // Only install mouse helper in headless mode
        if (this.launchConfig?.options?.headless) {
          installMouseHelper(page, this.launchConfig?.deviceConfig?.device || "desktop");
        }

        if (this.launchConfig?.customHeaders) {
          await page.setExtraHTTPHeaders({
            ...env.DEFAULT_HEADERS,
            ...this.launchConfig.customHeaders,
          });
        } else if (env.DEFAULT_HEADERS) {
          await page.setExtraHTTPHeaders(env.DEFAULT_HEADERS);
        }

        // Inject fingerprint only if it's not skipped
        if (!env.SKIP_FINGERPRINT_INJECTION) {
          // Use our safer fingerprint injection method instead of FingerprintInjector
          await this.injectFingerprintSafely(page, this.fingerprintData);
          this.logger.debug("[CDPService] Injected fingerprint into page");
        } else {
          this.logger.info(
            "[CDPService] Fingerprint injection skipped due to 'SKIP_FINGERPRINT_INJECTION' setting",
          );
        }

        await page.setRequestInterception(true);

        page.on("request", (request) => this.handlePageRequest(request, page));

        page.on("response", (response) => {
          if (response.url().startsWith("file://")) {
            this.logger.error(
              `[CDPService] Blocked response from file protocol: ${response.url()}`,
            );
            page.close().catch(() => {});
            this.shutdown();
          }
        });
      }
    } else if (target.type() === TargetType.BACKGROUND_PAGE) {
      this.logger.info(`[CDPService] Background page created: ${target.url()}`);
    }
  }

  private async handlePageRequest(request: HTTPRequest, page: Page) {
    const headers = request.headers();
    delete headers["accept-language"]; // Patch to help with headless detection

    const optimize = this.launchConfig?.optimizeBandwidth;
    const blockedHosts = typeof optimize === "object" ? optimize.blockHosts : undefined;
    const blockedUrlPatterns = typeof optimize === "object" ? optimize.blockUrlPatterns : undefined;

    if (this.launchConfig?.blockAds && isAdRequest(request.url())) {
      this.logger.info(`[CDPService] Blocked request to ad related resource: ${request.url()}`);
      await request.abort();
      return;
    }

    if (
      isHostBlocked(request.url(), blockedHosts) ||
      isUrlMatchingPatterns(request.url(), blockedUrlPatterns)
    ) {
      this.logger.info(`[CDPService] Blocked request to blocked host or pattern: ${request.url()}`);
      await request.abort();
      return;
    }

    // Block resources via optimizeBandwidth
    const blockImages = typeof optimize === "object" ? !!optimize.blockImages : false;
    const blockMedia = typeof optimize === "object" ? !!optimize.blockMedia : false;
    const blockStylesheets = typeof optimize === "object" ? !!optimize.blockStylesheets : false;

    if (blockImages || blockMedia || blockStylesheets) {
      const resourceType = request.resourceType();
      if (
        (blockImages && (resourceType === "image" || isImageRequest(request.url()))) ||
        (blockMedia && (resourceType === "media" || isHeavyMediaRequest(request.url()))) ||
        (blockStylesheets && resourceType === "stylesheet")
      ) {
        this.logger.info(
          `[CDPService] Blocked ${resourceType} resource due to optimizeBandwidth (${
            blockImages ? "blockImages" : ""
          }${blockMedia ? "blockMedia" : ""}${
            blockStylesheets ? "blockStylesheets" : ""
          }): ${request.url()}`,
        );
        await request.abort();
        return;
      }
    }

    if (request.url().startsWith("file://")) {
      this.logger.error(`[CDPService] Blocked request to file protocol: ${request.url()}`);
      page.close().catch(() => {});
      this.shutdown();
    } else {
      await request.continue({ headers });
    }
  }

  public async createPage(): Promise<Page> {
    if (!this.browserInstance) {
      throw new Error("Browser instance not initialized");
    }
    return this.browserInstance.newPage();
  }

  private async shutdownHook() {
    for (const mutator of this.shutdownMutators) {
      await mutator(this.currentSessionConfig);
    }
  }

  @traceable
  public async shutdown(): Promise<void> {
    this.shuttingDown = true;
    this.logger.info(`[CDPService] Shutting down and cleaning up resources`);

    try {
      if (this.browserInstance) {
        await this.pluginManager.onBrowserClose(this.browserInstance);
      }

      await this.pluginManager.onShutdown();

      this.removeAllHandlers();
      await this.browserInstance?.close();
      await this.browserInstance?.process()?.kill();
      await this.shutdownHook();

      this.logger.info("[CDPService] Cleaning up files during shutdown");
      try {
        await FileService.getInstance().cleanupFiles();
        this.logger.info("[CDPService] Files cleaned successfully");
      } catch (error) {
        this.logger.error(`[CDPService] Error cleaning files during shutdown: ${error}`);
      }

      this.fingerprintData = null;
      this.currentSessionConfig = null;
      this.browserInstance = null;
      this.wsEndpoint = null;
      this.emit("close");
      this.shuttingDown = false;
    } catch (error) {
      this.logger.error(`[CDPService] Error during shutdown: ${error}`);
      // Ensure we complete the shutdown even if plugins throw errors
      await this.browserInstance?.close();
      await this.browserInstance?.process()?.kill();
      await this.shutdownHook();

      try {
        await FileService.getInstance().cleanupFiles();
      } catch (cleanupError) {
        this.logger.error(
          `[CDPService] Error cleaning files during error recovery: ${cleanupError}`,
        );
      }

      this.browserInstance = null;
      this.shuttingDown = false;
    }
  }

  public getBrowserProcess() {
    return this.browserInstance?.process() || null;
  }

  public async createBrowserContext(proxyUrl: string): Promise<BrowserContext> {
    if (!this.browserInstance) {
      throw new Error("Browser instance not initialized");
    }
    return this.browserInstance.createBrowserContext({ proxyServer: proxyUrl });
  }

  @traceable
  public async launch(
    config?: BrowserLauncherOptions,
    retryOptions?: Partial<RetryOptions>,
  ): Promise<Browser> {
    const operation = async () => {
      try {
        return await this.launchInternal(config);
      } catch (error) {
        try {
          await this.pluginManager.onShutdown();
          await this.shutdownHook();
        } catch (e) {
          this.logger.warn(
            `[CDPService] Error during retry cleanup (onShutdown/shutdownHook): ${e}`,
          );
        }
        throw error;
      }
    };

    // Use retry mechanism for the launch process
    const result = await this.retryManager.executeWithRetry(
      operation,
      "Browser Launch",
      retryOptions,
    );

    return result.result;
  }

  @traceable
  private async launchInternal(config?: BrowserLauncherOptions): Promise<Browser> {
    try {
      const launchTimeout = new Promise<never>((_, reject) => {
        setTimeout(() => reject(new LaunchTimeoutError(60000)), 60000);
      });

      const launchProcess = (async () => {
        const shouldReuseInstance =
          this.browserInstance &&
          (await isSimilarConfig(this.launchConfig, config || this.defaultLaunchConfig));

        if (shouldReuseInstance) {
          this.logger.info(
            "[CDPService] Reusing existing browser instance with default configuration.",
          );
          this.launchConfig = config || this.defaultLaunchConfig;

          await executeCritical(
            async () => this.refreshPrimaryPage(),
            (error) =>
              new BrowserProcessError(
                "Failed to refresh primary page when reusing browser instance",
                BrowserProcessState.PAGE_REFRESH,
                error,
              ),
          );

          // Session context injection - should throw error if it fails
          if (this.launchConfig?.sessionContext) {
            this.logger.debug(
              `[CDPService] Session created with session context, injecting session context`,
            );
            await executeCritical(
              async () =>
                this.injectSessionContext(this.primaryPage!, this.launchConfig!.sessionContext!),
              (error) => {
                const contextError = new SessionContextError(
                  error instanceof Error ? error.message : String(error),
                  SessionContextType.CONTEXT_INJECTION,
                  error,
                );
                this.logger.warn(`[CDPService] ${contextError.message} - throwing error`);
                return contextError;
              },
            );
          }
          await this.pluginManager.onBrowserReady(this.launchConfig);

          return this.browserInstance!;
        } else if (this.browserInstance) {
          this.logger.info(
            "[CDPService] Existing browser instance detected. Closing it before launching a new one.",
          );
          await executeBestEffort(
            this.logger,
            async () => this.shutdown(),
            "Error during shutdown before launch",
          );
        }

        this.launchConfig = config || this.defaultLaunchConfig;
        this.logger.info("[CDPService] Launching new browser instance.");

        // Validate configuration
        await executeCritical(
          async () => validateLaunchConfig(this.launchConfig!),
          (error) => categorizeError(error, "configuration validation"),
        );

        // File cleanup - non-critical, log errors but continue
        this.logger.info("[CDPService] Cleaning up files before browser launch");
        await executeOptional(
          this.logger,
          async () => {
            await FileService.getInstance().cleanupFiles();
            this.logger.info("[CDPService] Files cleaned successfully before launch");
          },
          (error) =>
            new CleanupError(
              error instanceof Error ? error.message : String(error),
              CleanupType.PRE_LAUNCH_FILE_CLEANUP,
              error,
            ),
        );

        const { options, userAgent, userDataDir, fingerprint } = this.launchConfig;
        this.fingerprintData = fingerprint ?? null;

        // Run launch mutators - plugin errors should be caught
        await executeCritical(
          async () => {
            for (const mutator of this.launchMutators) {
              await mutator(this.launchConfig!);
            }
          },
          (error) =>
            new PluginError(
              error instanceof Error ? error.message : String(error),
              PluginName.LAUNCH_MUTATOR,
              PluginOperation.PRE_LAUNCH_HOOK,
              true,
              error,
            ),
        );

        // Fingerprint generation - can fail gracefully
        if (
          !env.SKIP_FINGERPRINT_INJECTION &&
          !userAgent &&
          !this.launchConfig.skipFingerprintInjection &&
          !this.fingerprintData
        ) {
          await executeCritical(
            async () => {
              let fingerprintOptions: Partial<FingerprintGeneratorOptions> = {
                devices: ["desktop"],
                operatingSystems: ["linux"],
                browsers: [{ name: "chrome", minVersion: 136 }],
                locales: ["en-US", "en"],
                screen: {
                  minWidth: this.launchConfig!.dimensions?.width ?? 1920,
                  minHeight: this.launchConfig!.dimensions?.height ?? 1080,
                  maxWidth: this.launchConfig!.dimensions?.width ?? 1920,
                  maxHeight: this.launchConfig!.dimensions?.height ?? 1080,
                },
              };

              if (this.launchConfig!.deviceConfig?.device === "mobile") {
                fingerprintOptions = {
                  devices: ["mobile"],
                  locales: ["en-US", "en"],
                };
              }

              const fingerprintGen = new FingerprintGenerator(fingerprintOptions);
              this.fingerprintData = fingerprintGen.getFingerprint();
            },
            (error) => {
              this.logger.error({ err: error }, "[CDPService] Error generating fingerprint");
              return new FingerprintError(
                error instanceof Error ? error.message : String(error),
                FingerprintStage.GENERATION,
                error,
              );
            },
          );
        } else if (this.fingerprintData) {
          this.logger.info(
            `[CDPService] Using existing fingerprint with user agent: ${this.fingerprintData.fingerprint.navigator.userAgent}`,
          );
        }

        const isHeadless = !!this.launchConfig?.options?.headless;

        this.currentSessionConfig = {
          ...this.launchConfig,
          dimensions: this.launchConfig.dimensions || this.fingerprintData?.fingerprint.screen,
          userAgent:
            this.launchConfig.userAgent || this.fingerprintData?.fingerprint.navigator.userAgent,
        };

        const extensionPaths = await executeCritical(
          async () => {
            const defaultExtensions = isHeadless ? ["recorder"] : [];
            const customExtensions = this.launchConfig!.extensions
              ? [...this.launchConfig!.extensions]
              : [];

            // Get named extension paths
            const namedExtensionPaths = await getExtensionPaths([
              ...defaultExtensions,
              ...customExtensions,
            ]);

            // Check for session extensions passed from the API
            let sessionExtensionPaths: string[] = [];
            if (this.launchConfig!.extra?.orgExtensions?.paths) {
              sessionExtensionPaths = this.launchConfig!.extra.orgExtensions
                .paths as unknown as string[];
              this.logger.info(
                `[CDPService] Found ${sessionExtensionPaths.length} session extension paths`,
              );
            }

            return [...namedExtensionPaths, ...sessionExtensionPaths];
          },
          (error) =>
            new ResourceError(
              `Failed to resolve extension paths: ${error}`,
              ResourceType.EXTENSIONS,
              false,
              error,
            ),
        );

        let timezone = this.defaultTimezone;
        if (config?.timezone) {
          const validatedTimezone = await executeOptional(
            this.logger,
            async () => {
              if (this.launchConfig?.skipFingerprintInjection) {
                this.logger.info(
                  `Skipping timezone validation as skipFingerprintInjection is enabled`,
                );
                return this.defaultTimezone;
              }
              const tz = await validateTimezone(this.logger, config.timezone!);
              this.logger.info(`Resolved and validated timezone: ${tz}`);
              return tz;
            },
            (error) => {
              this.logger.warn(`Timezone validation failed, using fallback`);
              return categorizeError(error, "timezone validation");
            },
            this.defaultTimezone,
          );
          timezone = validatedTimezone ?? this.defaultTimezone;
        }

        const extensionArgs = extensionPaths.length
          ? [
              `--load-extension=${extensionPaths.join(",")}`,
              `--disable-extensions-except=${extensionPaths.join(",")}`,
            ]
          : [];

        const shouldDisableSandbox = typeof process.getuid === "function" && process.getuid() === 0;

        const staticDefaultArgs = [
          "--remote-allow-origins=*",
          "--disable-dev-shm-usage",
          "--disable-gpu",
          "--disable-features=TranslateUI,BlinkGenPropertyTrees,LinuxNonClientFrame,PermissionPromptSurvey,IsolateOrigins,site-per-process,TouchpadAndWheelScrollLatching,TrackingProtection3pcd,InterestFeedContentSuggestions,PrivacySandboxSettings4,AutofillServerCommunication,OptimizationHints,MediaRouter,DialMediaRouteProvider,CertificateTransparencyComponentUpdater,GlobalMediaControls,AudioServiceOutOfProcess,LazyFrameLoading,AvoidUnnecessaryBeforeUnloadCheckSync",
          "--enable-features=Clipboard",
          "--no-default-browser-check",
          "--disable-sync",
          "--disable-translate",
          "--no-first-run",
          "--disable-search-engine-choice-screen",
          "--webrtc-ip-handling-policy=disable_non_proxied_udp",
          "--force-webrtc-ip-handling-policy",
          "--disable-touch-editing",
          "--disable-touch-drag-drop",
          "--disable-client-side-phishing-detection",
          "--disable-default-apps",
          "--disable-component-update",
          "--disable-infobars",
          "--disable-breakpad",
          "--disable-background-networking",
          "--disable-session-crashed-bubble",
          "--disable-ipc-flooding-protection",
          "--disable-popup-blocking",
          "--disable-prompt-on-repost",
          "--disable-domain-reliability",
          "--metrics-recording-only",
          "--no-pings",
          "--disable-backing-store-limit",
          "--password-store=basic",
          ...(shouldDisableSandbox
            ? ["--no-sandbox", "--disable-setuid-sandbox", "--no-zygote"]
            : []),
        ];

        const headfulArgs = [
          "--ozone-platform=x11",
          "--disable-renderer-backgrounding",
          "--disable-backgrounding-occluded-windows",
          "--use-gl=swiftshader",
          "--in-process-gpu",
          "--enable-crashpad",
          "--crash-dumps-dir=/tmp/chrome-dumps",
          "--noerrdialogs",
          "--force-device-scale-factor=1",
          "--disable-hang-monitor",
        ];

        const headlessArgs = [
          "--headless=new",
          "--hide-crash-restore-bubble",
          "--disable-blink-features=AutomationControlled",
          // can we just remove this outright?
          `--unsafely-treat-insecure-origin-as-secure=http://localhost:3000,http://${env.HOST}:${env.PORT}`,
        ];

        const dynamicArgs = [
          this.launchConfig.dimensions ? "" : "--start-maximized",
          `--remote-debugging-address=${env.HOST}`,
          "--remote-debugging-port=9222",
          `--window-size=${this.launchConfig.dimensions?.width ?? 1920},${
            this.launchConfig.dimensions?.height ?? 1080
          }`,
          userAgent ? `--user-agent=${userAgent}` : "",
          this.launchConfig.options.proxyUrl
            ? `--proxy-server=${this.launchConfig.options.proxyUrl}`
            : "",
        ];

        const uniq = (xs: string[]) => Array.from(new Set(xs.filter(Boolean)));

        const launchArgs = uniq([
          ...staticDefaultArgs,
          ...(isHeadless ? headlessArgs : headfulArgs),
          ...dynamicArgs,
          ...extensionArgs,
          ...(options.args || []),
          ...(env.CHROME_ARGS || []),
        ]).filter((arg) => !env.FILTER_CHROME_ARGS.includes(arg));

        const finalLaunchOptions = {
          ...options,
          defaultViewport: null,
          args: launchArgs,
          executablePath: this.chromeExecPath,
          ignoreDefaultArgs: ["--enable-automation"],
          timeout: 0,
          env: {
            HOME: os.userInfo().homedir,
            TZ: timezone,
            ...(isHeadless ? {} : { DISPLAY: env.DISPLAY }),
          },
          userDataDir,
          dumpio: env.DEBUG_CHROME_PROCESS, // Enable Chrome process stdout and stderr
        };

        this.logger.info(`[CDPService] Launch Options:`);
        this.logger.info(JSON.stringify(finalLaunchOptions, null, 2));

        if (userDataDir && this.launchConfig.userPreferences) {
          this.logger.info(`[CDPService] Setting up user preferences in ${userDataDir}`);
          await executeBestEffort(
            this.logger,
            async () => this.setupUserPreferences(userDataDir, this.launchConfig!.userPreferences!),
            "Failed to set up user preferences",
          );
        }

        // Browser process launch - most critical step
        this.browserInstance = await executeCritical(
          async () =>
            (await tracer.startActiveSpan("CDPService.launchBrowser", async () => {
              return await puppeteer.launch(finalLaunchOptions);
            })) as unknown as Browser,
          (error) =>
            new BrowserProcessError(
              error instanceof Error ? error.message : String(error),
              BrowserProcessState.LAUNCH_FAILED,
              error,
            ),
        );

        // Plugin notifications - catch individual plugin errors
        await executeOptional(
          this.logger,
          async () => this.pluginManager.onBrowserLaunch(this.browserInstance!),
          (error) =>
            new PluginError(
              error instanceof Error ? error.message : String(error),
              PluginName.PLUGIN_MANAGER,
              PluginOperation.BROWSER_LAUNCH_NOTIFICATION,
              true,
              error,
            ),
        );

        this.browserInstance.on("error", (err) => {
          this.logger.error(`[CDPService] Browser error: ${err}`);
          const error = err as Error;
          this.instrumentationLogger.record({
            type: BrowserEventType.BrowserError,
            error: { message: error?.message, stack: error?.stack },
            timestamp: new Date().toISOString(),
          });
        });

        this.primaryPage = await executeCritical(
          async () => (await this.browserInstance!.pages())[0],
          (error) =>
            new BrowserProcessError(
              "Failed to get primary page from browser instance",
              BrowserProcessState.PAGE_ACCESS,
              error,
            ),
        );

        // Session context injection - should throw error if it fails
        if (this.launchConfig?.sessionContext) {
          this.logger.debug(
            `[CDPService] Session created with session context, injecting session context`,
          );
          await executeCritical(
            async () =>
              this.injectSessionContext(this.primaryPage!, this.launchConfig!.sessionContext!),
            (error) => {
              const contextError = new SessionContextError(
                error instanceof Error ? error.message : String(error),
                SessionContextType.CONTEXT_INJECTION,
                error,
              );
              this.logger.warn(`[CDPService] ${contextError.message} - throwing error`);
              return contextError;
            },
          );
        }

        // Configure browser download behavior
        await executeBestEffort(
          this.logger,
          async () => {
            const downloadPath = FileService.getInstance().getBaseFilesPath();
            const cdpSession = await this.browserInstance!.target().createCDPSession();
            await cdpSession.send("Browser.setDownloadBehavior", {
              behavior: "allow",
              downloadPath: downloadPath,
              eventsEnabled: true,
            });
            await cdpSession.detach();
            this.logger.debug(
              `[CDPService] Download behavior configured with path: ${downloadPath}`,
            );
          },
          "Failed to configure download behavior",
        );

        this.browserInstance.on("targetcreated", this.handleNewTarget.bind(this));
        this.browserInstance.on("targetchanged", this.handleTargetChange.bind(this));
        this.browserInstance.on("targetdestroyed", (target) => {
          const targetId = (target as any)._targetId;
          this.targetInstrumentationManager.detach(targetId);
        });
        this.browserInstance.on("disconnected", this.onDisconnect.bind(this));

        this.wsEndpoint = await executeCritical(
          async () => this.browserInstance!.wsEndpoint(),
          (error) =>
            new NetworkError(
              "Failed to get WebSocket endpoint from browser",
              NetworkOperation.WEBSOCKET_SETUP,
              error,
            ),
        );

        // Final setup steps
        await executeOptional(
          this.logger,
          async () => {
            await this.handleNewTarget(this.primaryPage!.target());
            await this.handleTargetChange(this.primaryPage!.target());
          },
          (error) =>
            new BrowserProcessError(
              error instanceof Error ? error.message : String(error),
              BrowserProcessState.TARGET_SETUP,
              error,
            ),
        );

        try {
          const existingTargets = await this.browserInstance.targets();
          for (const target of existingTargets) {
            if ((target as any)._targetId !== (this.primaryPage.target() as any)._targetId) {
              await this.targetInstrumentationManager.attach(target, target.type() as TargetType);
            }
          }
          this.logger.info(
            `[CDPService] Attached instrumentation to ${existingTargets.length} existing targets`,
          );
        } catch (error) {
          this.logger.error({ err: error }, `[CDPService] Error attaching to existing targets`);
        }

        await this.pluginManager.onBrowserReady(this.launchConfig);

        return this.browserInstance;
      })();

      return (await Promise.race([launchProcess, launchTimeout])) as Browser;
    } catch (error: unknown) {
      const categorizedError =
        error instanceof BaseLaunchError ? error : categorizeError(error, "browser launch");

      this.logger.error(
        {
          error: {
            errorType: categorizedError.type,
            isRetryable: categorizedError.isRetryable,
            context: categorizedError.context,
          },
        },
        `[CDPService] LAUNCH ERROR (${categorizedError.type}): ${categorizedError.message}`,
      );

      throw categorizedError;
    }
  }

  @traceable
  public async proxyWebSocket(req: IncomingMessage, socket: Duplex, head: Buffer): Promise<void> {
    if (this.proxyWebSocketHandler) {
      this.logger.info("[CDPService] Using custom WebSocket proxy handler");
      await this.proxyWebSocketHandler(req, socket, head);
      return;
    }

    if (!this.wsEndpoint) {
      throw new Error(`WebSocket endpoint not available. Ensure the browser is launched first.`);
    }

    const cleanupListeners = () => {
      this.browserInstance?.off("close", cleanupListeners);
      if (this.browserInstance?.process()) {
        this.browserInstance.process()?.off("close", cleanupListeners);
      }
      this.browserInstance?.off("disconnected", cleanupListeners);
      socket.off("close", cleanupListeners);
      socket.off("error", cleanupListeners);
      this.logger.info("[CDPService] WebSocket connection listeners cleaned up");
    };

    this.browserInstance?.once("close", cleanupListeners);
    if (this.browserInstance?.process()) {
      this.browserInstance.process()?.once("close", cleanupListeners);
    }
    this.browserInstance?.once("disconnected", cleanupListeners);
    socket.once("close", cleanupListeners);
    socket.once("error", cleanupListeners);

    // Increase max listeners
    if (this.browserInstance?.process()) {
      this.browserInstance.process()!.setMaxListeners(60);
    }

    this.wsProxyServer.ws(
      req,
      socket,
      head,
      {
        target: this.wsEndpoint,
      },
      (error) => {
        if (error) {
          this.logger.error(`WebSocket proxy error: ${error}`);
          cleanupListeners(); // Clean up on error too
        }
      },
    );

    socket.on("error", (error) => {
      this.logger.error(`Socket error: ${error}`);
      // Try to end the socket properly on error
      try {
        socket.end();
      } catch (e) {
        this.logger.error(`Error ending socket: ${e}`);
      }
    });
  }

  public getUserAgent() {
    return (
      this.currentSessionConfig?.userAgent || this.fingerprintData?.fingerprint.navigator.userAgent
    );
  }

  public getDimensions() {
    return this.currentSessionConfig?.dimensions || { width: 1920, height: 1080 };
  }

  public getFingerprintData(): BrowserFingerprintWithHeaders | null {
    return this.fingerprintData;
  }

  public async getCookies(): Promise<Protocol.Network.Cookie[]> {
    if (!this.primaryPage) {
      throw new Error("Primary page not initialized");
    }
    const client = await this.primaryPage.createCDPSession();
    const { cookies } = await client.send("Network.getAllCookies");
    await client.detach();
    return cookies;
  }

  public async getBrowserState(): Promise<SessionData> {
    if (!this.browserInstance || !this.primaryPage) {
      throw new Error("Browser or primary page not initialized");
    }

    const userDataDir = this.launchConfig?.userDataDir;

    if (!userDataDir) {
      this.logger.warn("No userDataDir specified, returning empty session data");
      return {};
    }

    try {
      this.logger.info(`[CDPService] Dumping session data from userDataDir: ${userDataDir}`);

      // Run session data extraction and CDP storage extraction in parallel
      const [cookieData, sessionData, storageData] = await Promise.all([
        this.getCookies(),
        this.chromeSessionService.getSessionData(userDataDir),
        this.getExistingPageSessionData(),
      ]);

      // Merge storage data with session data
      const result = {
        cookies: cookieData,
        localStorage: {
          ...(sessionData.localStorage || {}),
          ...(storageData.localStorage || {}),
        },
        sessionStorage: {
          ...(sessionData.sessionStorage || {}),
          ...(storageData.sessionStorage || {}),
        },
        indexedDB: {
          ...(sessionData.indexedDB || {}),
          ...(storageData.indexedDB || {}),
        },
      };

      this.logger.info("[CDPService] Session data dumped successfully");
      return result;
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.error(`[CDPService] Error dumping session data: ${errorMessage}`);
      return {};
    }
  }

  /**
   * Extract all storage data (localStorage, sessionStorage, IndexedDB) for all open pages
   */
  private async getExistingPageSessionData(): Promise<SessionData> {
    if (!this.browserInstance || !this.primaryPage) {
      return {};
    }

    const result: SessionData = {
      localStorage: {},
      sessionStorage: {},
      indexedDB: {},
    };

    try {
      const pages = await this.browserInstance.pages();

      const validPages = pages.filter((page) => {
        try {
          const url = page.url();
          return url && url.startsWith("http");
        } catch (e) {
          return false;
        }
      });

      this.logger.info(
        `[CDPService] Processing ${validPages.length} valid pages out of ${pages.length} total for storage extraction`,
      );

      const results = await Promise.all(
        validPages.map((page) => extractStorageForPage(page, this.logger)),
      );

      // Merge all results
      for (const item of results) {
        for (const domain in item.localStorage) {
          result.localStorage![domain] = {
            ...(result.localStorage![domain] || {}),
            ...item.localStorage![domain],
          };
        }

        for (const domain in item.sessionStorage) {
          result.sessionStorage![domain] = {
            ...(result.sessionStorage![domain] || {}),
            ...item.sessionStorage![domain],
          };
        }

        for (const domain in item.indexedDB) {
          result.indexedDB![domain] = [
            ...(result.indexedDB![domain] || []),
            ...item.indexedDB![domain],
          ];
        }
      }

      return result;
    } catch (error) {
      this.logger.error(`[CDPService] Error extracting storage with CDP: ${error}`);
      return result;
    }
  }

  public async getAllPages() {
    return this.browserInstance?.pages() || [];
  }

  @traceable
  public async startNewSession(sessionConfig: BrowserLauncherOptions): Promise<Browser> {
    this.currentSessionConfig = sessionConfig;
    this.trackedOrigins.clear(); // Clear tracked origins when starting a new session

    return this.launch(sessionConfig);
  }

  @traceable
  public async endSession(): Promise<void> {
    this.logger.info("Ending current session and resetting to default configuration.");
    const sessionConfig = this.currentSessionConfig!;

    this.sessionContext = await this.getBrowserState().catch(() => null);

    await this.shutdown();
    await this.pluginManager.onSessionEnd(sessionConfig);
    this.currentSessionConfig = null;
    this.sessionContext = null;
    this.trackedOrigins.clear();

    this.instrumentationLogger.resetContext();

    await this.launch(this.defaultLaunchConfig);
  }

  private async onDisconnect(): Promise<void> {
    this.logger.info("Browser disconnected. Handling cleanup.");

    if (this.shuttingDown) {
      return;
    }

    await this.disconnectHandler();
  }

  @traceable
  private async injectSessionContext(
    page: Page,
    context?: BrowserLauncherOptions["sessionContext"],
  ) {
    if (!context) return;

    const storageByOrigin = groupSessionStorageByOrigin(context);

    for (const origin of storageByOrigin.keys()) {
      this.trackedOrigins.add(origin);
    }

    const client = await page.createCDPSession();
    try {
      if (context.cookies?.length) {
        await client.send("Network.setCookies", {
          cookies: context.cookies.map((cookie) => ({
            ...cookie,
            partitionKey: cookie.partitionKey as unknown as Protocol.Network.Cookie["partitionKey"],
          })),
        });
        this.logger.info(`[CDPService] Set ${context.cookies.length} cookies`);
      }
    } catch (error) {
      this.logger.error(`[CDPService] Error setting cookies: ${error}`);
    } finally {
      await client.detach().catch(() => {});
    }

    this.logger.info(
      `[CDPService] Registered frame navigation handler for ${storageByOrigin.size} origins`,
    );
    page.on("framenavigated", (frame) => handleFrameNavigated(frame, storageByOrigin, this.logger));

    page.browser().on("targetcreated", async (target) => {
      if (target.type() === "page") {
        try {
          const newPage = await target.page();
          if (newPage) {
            newPage.on("framenavigated", (frame) =>
              handleFrameNavigated(frame, storageByOrigin, this.logger),
            );
          }
        } catch (err) {
          this.logger.error(`[CDPService] Error adding framenavigated handler to new page: ${err}`);
        }
      }
    });

    this.logger.debug("[CDPService] Session context injection setup complete");
  }

  @traceable
  private async injectFingerprintSafely(
    page: Page,
    fingerprintData: BrowserFingerprintWithHeaders | null,
  ) {
    if (!fingerprintData) return;

    try {
      const { fingerprint, headers } = fingerprintData;
      // TypeScript fix - access userAgent through navigator property
      const userAgent = fingerprint.navigator.userAgent;
      const userAgentMetadata = fingerprint.navigator.userAgentData;
      const { screen } = fingerprint;

      await page.setUserAgent(userAgent);

      const session = await page.createCDPSession();

      try {
        await session.send("Page.setDeviceMetricsOverride", {
          screenHeight: screen.height,
          screenWidth: screen.width,
          width: screen.width,
          height: screen.height,
          viewport: {
            width: screen.availWidth,
            height: screen.availHeight,
            scale: 1,
            x: 0,
            y: 0,
          },
          mobile: /phone|android|mobile/i.test(userAgent),
          screenOrientation:
            screen.height > screen.width
              ? { angle: 0, type: "portraitPrimary" }
              : { angle: 90, type: "landscapePrimary" },
          deviceScaleFactor: screen.devicePixelRatio,
        });

        const injectedHeaders = filterHeaders(headers);

        await page.setExtraHTTPHeaders(injectedHeaders);

        await session.send("Emulation.setUserAgentOverride", {
          userAgent: userAgent,
          acceptLanguage: headers["accept-language"],
          platform: fingerprint.navigator.platform || "Linux x86_64",
          userAgentMetadata: {
            brands:
              userAgentMetadata.brands as unknown as Protocol.Emulation.UserAgentMetadata["brands"],
            fullVersionList:
              userAgentMetadata.fullVersionList as unknown as Protocol.Emulation.UserAgentMetadata["fullVersionList"],
            fullVersion: userAgentMetadata.uaFullVersion,
            platform: fingerprint.navigator.platform || "Linux x86_64",
            platformVersion: userAgentMetadata.platformVersion || "",
            architecture: userAgentMetadata.architecture || "x86",
            model: userAgentMetadata.model || "",
            mobile: userAgentMetadata.mobile as unknown as boolean,
            bitness: userAgentMetadata.bitness || "64",
            wow64: false, // wow64 property doesn't exist on UserAgentData, defaulting to false
          },
        });
      } finally {
        // Always detach the session when done
        await session.detach().catch(() => {});
      }

      await page.evaluateOnNewDocument(
        loadFingerprintScript({
          fixedPlatform: fingerprint.navigator.platform || "Linux x86_64",
          fixedVendor: (fingerprint.videoCard as VideoCard | null)?.vendor,
          fixedRenderer: (fingerprint.videoCard as VideoCard | null)?.renderer,
          fixedDeviceMemory: fingerprint.navigator.deviceMemory || 8,
          fixedHardwareConcurrency: fingerprint.navigator.hardwareConcurrency || 8,
          fixedArchitecture: userAgentMetadata.architecture || "x86",
          fixedBitness: userAgentMetadata.bitness || "64",
          fixedModel: userAgentMetadata.model || "",
          fixedPlatformVersion: userAgentMetadata.platformVersion || "15.0.0",
          fixedUaFullVersion: userAgentMetadata.uaFullVersion || "131.0.6778.86",
          fixedBrands:
            userAgentMetadata.brands ||
            ([] as unknown as Array<{
              brand: string;
              version: string;
            }>),
        }),
      );
    } catch (error) {
      this.logger.error({ error }, `[Fingerprint] Error injecting fingerprint safely`);
      const fingerprintInjector = new FingerprintInjector();
      // @ts-ignore - Ignore type mismatch between puppeteer versions
      await fingerprintInjector.attachFingerprintToPuppeteer(page, fingerprintData);
    }
  }

  @traceable
  private async setupUserPreferences(userDataDir: string, userPreferences: Record<string, any>) {
    try {
      const preferencesPath = getProfilePath(userDataDir, "Preferences");
      const defaultProfileDir = path.dirname(preferencesPath);

      await fs.promises.mkdir(defaultProfileDir, { recursive: true });

      let existingPreferences = {};

      try {
        const existingContent = await fs.promises.readFile(preferencesPath, "utf8");
        existingPreferences = JSON.parse(existingContent);
      } catch (error) {
        this.logger.debug(`[CDPService] No existing preferences found, creating new: ${error}`);
      }

      const mergedPreferences = deepMerge(existingPreferences, userPreferences);

      await fs.promises.writeFile(preferencesPath, JSON.stringify(mergedPreferences, null, 2));

      this.logger.info(`[CDPService] User preferences written to ${preferencesPath}`);
    } catch (error) {
      this.logger.error(`[CDPService] Error setting up user preferences: ${error}`);
      throw error;
    }
  }
}
